<!DOCTYPE html>
<!--Copyright 2010 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.-->
<html>
<head>
<script src="jsonrpc_server.js" type="text/javascript"></script>
<script type="text/javascript">
/* jslint globals: jsonrpc_server, window, getPostMessageReceiver */
(function() {
  var rpcserver = jsonrpc_server();
  rpcserver.addMessageReceiver(getPostMessageReceiver('*')); //possibly restrict via url param
  var myStorage = window.localStorage;
  var REG_STORE_PREFIX = "inreg:";
  
  //////////// BEGIN VALIDATION AND UTILITY FUNCTION ////////////
  
  function isArray(obj) {
    return typeof obj === 'object' && obj.constructor.toString().indexOf("Array") !== -1;
  }
  
  var isValidMediaRange = (function() { // does not allow "*/*"
    var validMediaRange = new RegExp("^([^\*]+)/(.+)$");
    return function(mr) {
      return validMediaRange.test(mr);
    };
  }());
  
  function isMediaRangeList(mrl) {
    if (!isArray(mrl)) { return false; }
    if (mrl.length < 1) { return false; }
    for (var i = mrl.length; i--;) {
      if (!isValidMediaRange(mrl[i])) {
        return false;
      }
    }
    return true; //TODO
  }
  
  var parseMediaRange = (function() { // does not allow "*/*"
    var parseExp = new RegExp("^([^\*]+)/(.+)$");
    return function(mr) {
      var parsed = parseExp.exec(mr);
      if (parsed === null) { throw "ParseError"; }
      return {
        type: parsed[0],
        subtype: parsed[1]
      };
    };
  }());
  
  function matchMediaRange(a, b) {
    try {
      a = parseMediaRange(a);
      b = parseMediaRange(b);
      return a.type === b.type && (a.subtype === b.subtype || a.subtype === "*" || b.subtype === "*");
    } catch(e) {
      return false;
    }
  }
  
  function matchMediaRangeList(alist, blist) {
    for (var i = alist.length; i--;) {
      for (var j = blist.length; j--;) {
        if (matchMediaRange(alist[i], blist[j])) {
          return true;
        }
      }
    }
    return false;
  }
  
  function isValidURI(uri) {
    if (typeof uri !== "string") { return false; }
    return true; //TODO
  }
  
  function getOrigin(l) {
    return l.protocol + "//" + l.host + (l.port && ":" + l.port);
  }
  
  RegExp.escape = (function() {
    var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g"); // .*+?|()[]{}\
    return function(str) {
      return str.replace(specials, "\\$&");
    };
  }());
  
  if(!Array.indexOf){
    Array.prototype.indexOf = function(obj){
      for(var i=0; i<this.length; i++){
        if(this[i]===obj){
          return i;
        }
      }
      return -1;
    };
  }
  
  function getOriginFromURI(uri) {
    var docPortionStart = uri.indexOf("/", 8);
    if (docPortionStart === -1) {
      return uri;
    }
    return uri.substring(0, docPortionStart);
  }
  
  //////////// END VALIDATION AND UTILITY FUNCTION ////////////
  
  var consent = (function() {
    var capi = {};
    
    capi.registrationConsent = function(registration, callback) {
      //implement better
      callback(confirm(registration.name + " is attempting to register it self with Introducer. Allow?"));
    };
    
    return capi;
  }());
  
  var introducer = (function() {
    var idb = {};
    var pendingRequests = [];
    var providerInstances = [];
    var providerWindowMapping = {};
    var nextRequestId = 0;
    
    function getRegistrationKey(origin, id) {
      return REG_STORE_PREFIX + origin + "#" + id;
    }
    
    function setupProvider(requestId, provider, request) {
      
      providerInstances[requestId] = {
        provider: provider,
        request: request,
        requestId: requestId
      };
      request.callback({ 'type': 'showProvider', 'requestId': requestId, 'popup': provider.popup || null });
    }
    
    idb.register = function(meta, sender, registration) { //stateless
      if (!meta.origin) { throw "SecurityError: no verifiable origin of request"; }
      if (registration === null || typeof registration !== "object") { throw "TypeError: registration is not a valid object"; }
      if (!registration.id  || typeof registration.id !== "string") { throw "TypeError: registration.id is not a valid string"; } //required
      if (!registration.icon || !isValidURI(registration.icon)) { throw "TypeError: registration.icon is not a valid URI"; } //required
      //TODO: need a way to validate/store icon image [cannot change] (maybe host a to data: convertor on webintroducer.org)
      if (!registration.name || typeof registration.name !== "string") { throw "TypeError: registration.name is not a valid string"; } //required
      if (registration.description && typeof registration.description !== "string") { throw "TypeError: registration.description is not a valid string"; } //optional
      if (!registration.home || !isValidURI(registration.home)) { throw "TypeError: registration.home is not a valid URI"; } //required
      if (!registration.providers || !isArray(registration.providers)) { throw "TypeError: registration.providers is not an array"; } // required
      if (registration.providers.length < 1) { throw "Error: registration.providers array is empty."; }
      
      var newProviders = [];
      var provider;
      for (var p = registration.providers.length; p--;) {
        provider = registration.providers[p];
        if (provider === null || typeof provider !== "object") { throw "TypeError: provider is not a valid object"; } //required
        if (!provider.id  || typeof provider.id !== "string") { throw "TypeError: provider.id is not a valid string"; } //required
        if (!provider.name || typeof provider.name !== "string") { throw "TypeError: provider.name is not a valid string"; } //required
        if (provider.description && typeof provider.description !== "string") { throw "TypeError: provider.description is not a valid string"; } //optional
        if (!provider.supports || !isMediaRangeList(provider.supports)) { throw "TypeError: provider.supports is not a valid media range list"; } // required
        if (!provider.URI || !isValidURI(provider.URI)) { throw "TypeError: provider.URI is not a valid URI"; } //required
        //TODO: add popup check
        newProviders.push({
          id: provider.id,
          name: provider.name,
          description: provider.description,
          supports: provider.supports,
          popup: provider.popup || null,
          URI: provider.URI
        });
      }
      
      var regKey = getRegistrationKey(meta.origin, registration.id);
      var eReg = myStorage[regKey];
      eReg = eReg ? JSON.parse(eReg) : null;
      
      var storeRegistration = function() {
        var currentTimestamp = new Date().getTime();
      
        var newRegistration = {
          creation_date: eReg ? eReg.creation_date : currentTimestamp,
          update_date: currentTimestamp,
          origin: meta.origin,
          id: registration.id,
          icon: eReg ? eReg.icon : registration.icon,
          name: eReg ? eReg.name : registration.name,
          description: registration.description || "",
          home: registration.home,
          providers: newProviders
        };
        delete myStorage['cache:idlist:' + meta.origin];
        myStorage[regKey] = JSON.stringify(newRegistration);
        delete myStorage['cache:idlist:' + meta.origin];  // twice to prevent race condition
        meta.callback(true);
      };
      
      if (eReg !== null) {
        storeRegistration();
      } else {
        consent.registrationConsent(registration, function(allow) {
          if (allow) {
            storeRegistration();
          } else {
            //silently don't store registration
          }
        });
      }
    };

    idb.transfer = function(meta, registrationId, newOrigin) { //stateless
      if (!registrationId  || typeof registrationId !== "string") { throw "TypeError: registrationId is not a valid string"; } //required
      if (!newOrigin || typeof newOrigin !== "string") { throw "TypeError: newOrigin is not a valid HTML5 origin."; }
      
      var regKey = getRegistrationKey(meta.origin, registrationId);
      
      /*
      // Delete Registration if newOrigin is not specified
      if (!newOrigin) {
        delete myStorage['cache:idlist:' + meta.origin];
        delete myStorage[regKey];
        delete myStorage['cache:idlist:' + meta.origin];  // twice to prevent race condition
        return;
      }
      */
      
      var newKey = getRegistrationKey(newOrigin, registrationId);
      var eRegData = myStorage[regKey];
      var exists = myStorage[newKey];
      
      // Complete transfer if original registration exists and
      // conflicting destination registration doesn't exist
      // If conflicting registration exists, delete origin registration
      if (eRegData) {
        delete myStorage['cache:idlist:' + meta.origin];
        delete myStorage[regKey];
        if (!exists) {
          myStorage[newKey] = eRegData;
        }
        delete myStorage['cache:idlist:' + meta.origin]; // twice to prevent race condition
      }
    };
    
    idb.remove = function(meta, registrationId) { //stateless
      if (!registrationId  || typeof registrationId !== "string") { throw "TypeError: registrationId is not a valid string"; } //required
      var regKey = getRegistrationKey(meta.origin, registrationId);
      delete myStorage['cache:idlist:' + meta.origin];
      delete myStorage[regKey];
      delete myStorage['cache:idlist:' + meta.origin]; // twice to prevent race condition
    };

    idb.request = function(meta, sender, types) { //stores state
      if (!meta.origin) { throw "SecurityError: no verifiable origin of request"; }
      if (!types || !isMediaRangeList(types)) { throw "TypeError: types is not a valid media range list"; } // required

      var registration;
      var key;
      var matcher = new RegExp('^' + RegExp.escape(REG_STORE_PREFIX));
      var matchingProviders = [];
      var provider;
      for (var i = myStorage.length; i--;) {
        key = myStorage.key(i);
        if (matcher.test(key)) {
          registration = JSON.parse(myStorage[key]);
          for (var p = registration.providers.length; p--;) {
            if (matchMediaRangeList(registration.providers[p].supports, types)) {
              provider = registration.providers[p];
              provider.registration = {};
              provider.registration.name = registration.name;
              provider.registration.icon = registration.icon;
              provider.registration.home = registration.home;
              provider.registration.description = registration.description;
              provider.registration.id = registration.id;
              provider.registration.origin = registration.origin;
              provider.registration.key = key;
              matchingProviders.push(provider);
            }
          }
        }
      }
      
      if (matchingProviders.length === 0) {
        return null; // tell requesting page that request could not be fulfilled
      }
      
      var requestId = nextRequestId;
      nextRequestId = nextRequestId + 1;
      
      pendingRequests[requestId] = {
        origin: meta.origin,
        types: types,
        matchingProviders: matchingProviders,
        callback: meta.callback
        //should we save requesting window so we can verify iframe is opened in the correct window
        //->it shouldn't matter because request only exists in the context of current page
      };
      return { 'type': 'showMenu', 'requestId': requestId }; // provide requesting page requestId so iframe can be opened
    };

    idb.getSupportedTypes = function() { //stateless
      var registration;
      var key;
      var supports = [];
      var typelist;
      var matcher = new RegExp('^' + RegExp.escape(REG_STORE_PREFIX));
      for (var i = myStorage.length; i--;) {
        key = myStorage.key(i);
        if (matcher.test(key)) {
          registration = JSON.parse(myStorage[key]);
          for (var p = registration.providers.length; p--;) {
            typelist = registration.providers[p].supports;
            for (var t = typelist.length; t--;) {
              if (supports.indexOf(typelist[t]) === -1) {
                supports.push(typelist[t]);
              }
            }
          }
        }
      }
      return supports;
      //return myStorage['supports:all'];
    };

    idb.getRegisteredIds = function(meta) { //stateless
      if (!meta.origin) { throw "SecurityError: no verifiable origin of request"; }
      var cache = myStorage['cache:idlist:' + meta.origin];
      var cacheLock = false;
      if (cache !== "locked") {
        if (cache) {
          return JSON.parse(cache);
        }
        myStorage['cache:idlist:' + meta.origin] = "locked";
        cacheLock = true;
      }
      
      var registered_ids = [];
      var key;
      var match_string = REG_STORE_PREFIX + meta.origin + "#";
      var matcher = new RegExp('^' + RegExp.escape(match_string));
      for (var i = myStorage.length; i--;) {
        key = myStorage.key(i);
        if (matcher.test(key)) {
          registered_ids.push(key.substring(match_string.length));
        }
      }
      if (cacheLock && myStorage['cache:idlist:' + meta.origin] === "locked") {
        myStorage['cache:idlist:' + meta.origin] = JSON.stringify(registered_ids);
      }
      return registered_ids;
    };

    idb.receiveCustomerMessages = function(meta, requestId, registrationOriginList, supportedTypes) {
      if (!isArray(registrationOriginList)) { throw "registrationOriginList is not an array"; } // required
      if (!supportedTypes || !isMediaRangeList(supportedTypes)) { throw "TypeError: supportedTypes is not a valid media range list"; } // required

      var providerInstance = providerInstances[requestId];
      if (!providerInstance) { throw "Error: provider not found."; }

      //NOTE: Is this check necessary, or does it only limit the Provider (should provider be able to redirect?)
      if (getOriginFromURI(providerInstance.provider.URI) !== meta.origin) {
        throw "SecurityError: origin does not match";
      }

      providerInstance.providerOrigin = meta.origin;
      if (registrationOriginList.indexOf(providerInstance.provider.registration.origin) === -1 && registrationOriginList.indexOf("*") === -1) {
        //unsupported registration origin
        console.log("receiveCustomerMessages unsupported registration origin"); //debug only
        return;
      }
      
      if (!matchMediaRangeList(supportedTypes, providerInstance.request.types)) {
        console.log("receiveCustomerMessages media range does not match"); //debug only
        return;
      }
      // If we return false, should we close out the provider, or should more than one call be allowed?
      providerInstance.providerMessageCB = meta.callback;
      providerInstance.providerWindow = meta.source;
      meta.callback(true);
      providerInstance.request.callback({ 'type': 'providerReady', 'requestId': providerInstance.requestId });
    };
    
    idb.receiveProviderMessages = function(meta, requestId) {
      var providerInstance = providerInstances[requestId];
      if (!providerInstance) { throw "Error: provider not found."; }
      if (meta.origin !== providerInstance.request.origin) { throw "SecurityError: Origin does not match request. "; }
      providerInstance.customerMessageCB = meta.callback;
      return true;
    };
    
    idb.sendMessage = function(meta, requestId, message, anonymous) {
      var providerInstance = providerInstances[requestId];
      if (!providerInstance) { throw "Error: provider not found."; }
      
      var outgoingEvent = {
        data: message,
        origin: anonymous ? null : meta.origin
      };
      
      if (meta.source === providerInstance.providerWindow) {
        // PROVIDER TO CUSTOMER
        if (meta.origin !== providerInstance.providerOrigin) { throw "SecurityError: Origin does not match provider."; }
        if (typeof providerInstance.customerMessageCB !== "function") { throw "Error: Customer not ready."; }
        providerInstance.customerMessageCB(outgoingEvent);
      } else {
        // CUSTOMER TO PROVIDER
        if (meta.origin !== providerInstance.request.origin) { throw "SecurityError: Origin does not match request. "; }
        if (typeof providerInstance.providerMessageCB !== "function") { throw "Error: Provider not ready."; }
        //outgoingEvent.request = {};
        //outgoingEvent.request.types = providerInstance.request.types;
        //outgoingEvent.request.registration = providerInstance.provider.registration;
        //outgoingEvent.request.provider = providerInstance.provider; //Note: also includes registration
        providerInstance.providerMessageCB(outgoingEvent);
      }
    };
    
    idb.getProviderList = function(meta, requestId) { //uses state
      if (meta.origin !== getOrigin(window.location)) { throw "SecurityError: Only Introducer pages may call getProviderList"; }
      if (!pendingRequests[requestId]) { throw "Error: pending request not found."; }
      return pendingRequests[requestId].matchingProviders;
    };
    
    idb.setChoosenProvider = function(meta, requestId, provider) {
      if (meta.origin !== getOrigin(window.location)) { throw "SecurityError: Only Introducer pages may call setChoosenProvider"; }
      if (!pendingRequests[requestId]) { throw "Error: pending request not found."; }
      if (typeof pendingRequests[requestId].callback === "function") {
        if (provider === null) {
          // Tell the requesting page that the request could not be fulfilled.
          // Requesting page should close provider menu iframe.
          pendingRequests[requestId].callback(null);
        }
        var currentRequest = pendingRequests[requestId];
        delete pendingRequests[requestId];
        setupProvider(requestId, provider, currentRequest);
        //TODO: all
      }
    };
    
    idb.getProviderURI = function(meta, requestId) {
      if (meta.origin !== getOrigin(window.location)) { throw "SecurityError: Only Introducer pages may call setChoosenProvider"; }
      if (!providerInstances[requestId]) { throw "Error: request not found."; }
      return providerInstances[requestId].provider.URI;
    };
    
    rpcserver.register("register", idb.register, idb, true);
    rpcserver.register("transfer", idb.transfer, idb, true);
    rpcserver.register("request", idb.request, idb, true);
    //rpcserver.register("getSupportedTypes", idb.getSupportedTypes, idb, false); // API REMOVED
    //rpcserver.register("getRegisteredIds", idb.getRegisteredIds, idb, true); // API REMOVED
    rpcserver.register("getProviderList", idb.getProviderList, idb, true);
    rpcserver.register("setChoosenProvider", idb.setChoosenProvider, idb, true);
    rpcserver.register("receiveCustomerMessages", idb.receiveCustomerMessages, idb, true);
    rpcserver.register("receiveProviderMessages", idb.receiveProviderMessages, idb, true);
    rpcserver.register("sendMessage", idb.sendMessage, idb, true);
    rpcserver.register("getProviderURI", idb.getProviderURI, idb, true);

    return idb;
  }());

  ia = introducer; // REMOVE LINE // TESTING ONLY
  
}());
</script>
</head>
<body>
</body>
</html>